﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

#if WINDOWS_PHONE
namespace Fibre.WP7.Threading
#else
#if SILVERLIGHT
namespace Fibre.Silverlight.Threading
#else
namespace Fibre.Threading
#endif
#endif
{
    /// <summary>
    /// Worker which performs multiple background operations simultanously.
    /// </summary>
    public class SimultaneousWorker 
    {
        private readonly Queue<WorkItem> _workItems = new Queue<WorkItem>();
        private readonly List<WorkItem> _runningWorkItems = new List<WorkItem>();
        private readonly ManualResetEvent _workWaitHandle = new ManualResetEvent(true);
        private readonly SynchronizationContext _uiContext;
        private readonly object _workLock = new object();
        private Thread _workThread;
        private bool _hasBeenAborted;
        private bool _isStarted;
        private bool _isBusy;
        private int _maxRunningWorkItems = 3;

        public SimultaneousWorker()
        {
            _uiContext = SynchronizationContext.Current;
        }

        public SimultaneousWorker(int maxRunningWorkItems)
        {
            _maxRunningWorkItems = maxRunningWorkItems;
            _uiContext = SynchronizationContext.Current;
        }

        public void SetMaxRunningWorkItems(int maxRunningWorkItems)
        {
            _maxRunningWorkItems = maxRunningWorkItems;
        }

        public bool Started
        {
            get { return _isStarted; }
        }

        public bool IsBusy
        {
            get { return _isBusy; }
            private set 
            {
                if (_isBusy == value)
                    return;

                _isBusy = value;
                var handler = BusyChanged;
                if (handler != null)
                    handler(this, new BusyChangedEventArgs(value));
            }
        }

        public event BusyChangedEventHandler BusyChanged; 

        /// <summary>
        /// Adds a work item to the SimultaneousWorker
        /// </summary>
        /// <param name="workItem">The work item.</param>
        /// <returns></returns>
        public SimultaneousWorker AddWork(WorkItem workItem)
        {
            lock (_workLock)
            {
                if (workItem.Work == null)
                    throw new InvalidOperationException("You must assign work items work");

                if (_workItems.SingleOrDefault(a => ReferenceEquals(a, workItem)) != null)
                    throw new InvalidOperationException("Do not add the same WorkItem twice");

                if (_isStarted)
                    StartWorkItem(workItem);
                else
                    _workItems.Enqueue(workItem);
            }

            return this;
        }

        /// <summary>
        /// Perform all work asynchronously. This method is non-blocking
        /// </summary>
        /// <param name="complete"></param>
        public void Start(Action complete)
        {
            lock (_workLock)
            {
                if (!_isStarted)
                {
                    _workWaitHandle.Reset();
                    ThreadPool.QueueUserWorkItem(t => StartWorker(complete));
                    _isStarted = true;
                    _hasBeenAborted = false;
                }

                StartWorkItems();
            }
        }

        private void StartWorkItems()
        {
            while (_workItems.Count > 0 && _runningWorkItems.Count < _maxRunningWorkItems)
            {
                StartWorkItem(_workItems.Dequeue());
            }
        }

        /// <summary>
        /// Stops the Simultaneous worker, any new work that is added will not be started right away
        /// </summary>
        public void Stop()
        {
            lock (_workLock)
            {
                _isStarted = false;
                _workWaitHandle.Set();
                _workWaitHandle.Reset();
            }
        }

        private void StartWorker(Action complete)
        {
            _workThread = Thread.CurrentThread;

            _workWaitHandle.WaitOne();

            //Invoke complete callback if has not been aborted
            if (complete != null && !_hasBeenAborted && _isStarted)
            {
                // If we have a UI dispatcher, call on UI Thread
                if (_uiContext != null)
                    _uiContext.Post(state => complete(), null);
                else
                    complete();
            }
        }

        /// <summary>
        /// Starts a work item.
        /// </summary>
        /// <param name="localItem">The work item to start.</param>
        private void StartWorkItem(WorkItem localItem)
        {
            IsBusy = true;
            _runningWorkItems.Add(localItem);
            //Work is an non blocking method call
            localItem.Work(result =>
                               {
                                   _runningWorkItems.Remove(localItem);
                                   if (localItem.CompletedEvents.Count != 0 && !_hasBeenAborted)
                                   {
                                       if (_uiContext != null)
                                           _uiContext.Post(state => RaiseCompletionEvents(localItem, result), null);
                                       else
                                           RaiseCompletionEvents(localItem, result);
                                   }
                                   else
                                       EvaluateComplete();
                               });
        }

        private void RaiseCompletionEvents(WorkItem localItem, IAsyncResult result)
        {
            foreach (var completedEvent in localItem.CompletedEvents)
            {
                completedEvent(result);
            }
            EvaluateComplete();
        }

        /// <summary>
        /// Evaluates if the SimultaneousWorker is complete
        /// </summary>
        private void EvaluateComplete()
        {
            lock (_workLock)
            {
                //Kick off any items waiting to be run
                StartWorkItems();
                if (_runningWorkItems.Count != 0) return;
                _workWaitHandle.Set();
            }
            IsBusy = false;
        }

        /// <summary>
        /// Work continues, but the managing thread will be killed.
        /// This means the completed callback will not be fired
        /// Individual work items will still fire their completed callbacks.
        /// </summary>
        public void AbortWork()
        {
            lock (_workLock)
            {
                _hasBeenAborted = true;
                if (_workThread == null) return;
                _workThread.Abort();
                _isStarted = false;
                _workItems.Clear();
                _runningWorkItems.Clear();
                IsBusy = false;
            }
        }

        /// <summary>
        /// Waits for the work to complete before returning.
        /// 
        /// If work has not been started this method will return instantly.
        /// </summary>
        public void WaitForCompletion()
        {
            if (_runningWorkItems.Count != 0)
                _workWaitHandle.WaitOne();
        }
    }
}
